package auth

import (
	"context"
	"coolcar/v1/shared/auth/token"
	"coolcar/v1/shared/ids"
	"github.com/dgrijalva/jwt-go"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"io/ioutil"
	"os"
	"strings"
)

const (
	ImpersonateAccountHeader = "impersonate-account-id"
	authorizationHeader      = "authorization"
	bearerPrefix             = "Bearer "
)

type interceptor struct {
	tokenVerifier
}

type tokenVerifier interface {
	Verify(token string) (ids.AccountID, error)
}

// Interceptor creates a grpc interceptor.
func Interceptor(publicKeyPath string) (grpc.UnaryServerInterceptor, error) {
	file, err := os.Open(publicKeyPath)
	if err != nil {
		return nil, err
	}
	bytes, err := ioutil.ReadAll(file)
	if err != nil {
		return nil, err
	}
	publicKey, err := jwt.ParseRSAPublicKeyFromPEM(bytes)
	if err != nil {
		return nil, err
	}
	i := interceptor{
		tokenVerifier: &token.JWTTokenVerifier{PublicKey: publicKey},
	}
	return i.HandleReq, nil
}

func (i *interceptor) HandleReq(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	aid := impersonationFromContext(ctx)
	if aid != "" {
		return handler(ContextWithAccountID(ctx, ids.AccountID(aid)), req)
	}
	tkn, err := tokenFromContext(ctx)
	if err != nil {
		return nil, status.Error(codes.Unauthenticated, "")
	}
	aidd, err := i.tokenVerifier.Verify(tkn)
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, "token not valid: %v", err)
	}
	return handler(ContextWithAccountID(ctx, aidd), req)
}

func impersonationFromContext(ctx context.Context) string {
	in, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return ""
	}

	imp := in[ImpersonateAccountHeader]
	if len(imp) == 0 {
		return ""
	}

	return imp[0]
}

type accountIDKey struct{}

// ContextWithAccountID creates a context with given account ID.
func ContextWithAccountID(ctx context.Context, aid ids.AccountID) context.Context {
	return context.WithValue(ctx, accountIDKey{}, aid)
}

// AccountIDFromContext returns an account ID from the context.Context.
// Return Unauthenticated error if no accountID  is available.
func AccountIDFromContext(ctx context.Context) (ids.AccountID, error) {
	v := ctx.Value(accountIDKey{})
	aid, ok := v.(ids.AccountID)
	if !ok {
		return "", status.Error(codes.Unauthenticated, "")
	}
	return aid, nil
}

func tokenFromContext(ctx context.Context) (string, error) {
	in, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", status.Error(codes.Unauthenticated, "")
	}
	tkn := ""
	for _, v := range in[authorizationHeader] {
		if strings.HasPrefix(v, bearerPrefix) {
			tkn = v[len(bearerPrefix):]
		}
	}
	if tkn == "" {
		return "", status.Error(codes.Unauthenticated, "")
	}
	return tkn, nil
}
